package com.hb.config;

import com.hb.Impl.SpitterUserService;
import com.hb.data.SpitterRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.encoding.Md5PasswordEncoder;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.StandardPasswordEncoder;

import javax.sql.DataSource;

/**
 * 1、Spring Security 的 Java 配置类。
 * Spring Security 必须配置在一个实现了 WebSecurityConfigurer 的 bean 中，或者（简单起见）扩展 WebSecurityConfigurerAdapter。
 * 在 Spring 应用上下文中，任何实现了 WebSecurityConfigurer 的 bean 都可以用来配置 Spring Security，但是最为简单的方式还是像下面
 * 这样扩展 WebSecurityConfigurerAdapter 类。
 * 2、我们可能希望指定 Web 安全的细节，这要通过重载 WebSecurityConfigurerAdapter 中的一个或多个方法来实现。我们可以通过重载
 * WebSecurityConfigurerAdapter 的三个 configure() 方法来配置 Web 安全性，这个过程中会使用传递进来的参数设置行为。这三个
 * configure() 方法如下：
 * ① configure(WebSecurity)：通过重载，配置 Spring Security 的 Filter 链。
 * ② configure(HttpSecurity)：通过重载，配置如何通过拦截器保护请求。
 * ③ configure(AuthenticationManagerBuilder)：通过重载，配置 user-detail 服务。
 */
@Configuration
// 启用 Web 安全性。@EnableWebSecurity 注解将会启用 Web 安全功能。但它本身并没有什么用处。@EnableWebSecurity 可以启用任意Web应用的安全性功能，
// 不过，如果你的应用碰巧是使用 Spring MVC 开发的，那么就应该考虑使用 @EnableWebMvcSecurity 替代它
//@EnableWebSecurity

// 启用 Spring MVC 安全性。
// 除了其他的内容以外，@EnableWebMvcSecurity 注解还配置了一个 Spring MVC 参数解析解析器（argument resolver），这样的话处理器方法就能够通过带
// 有 @AuthenticationPrincipal 注解的参数获得认证用户的 principal（或 username）。它同时还配置了一个 bean，在使用 Spring 表单绑定标签库来定
// 义表单时，这个 bean 会自动添加一个隐藏的跨站请求伪造（cross-site request forgery，CSRF）token输入域。
@EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    DataSource dataSource;

    @Autowired
    SpitterRepository spitterRepository;

    /**
     * 下面这个 configure(HttpSecurity http) 等同于默认的 configure(HttpSecurity http)，默认也就是不重载 configure(HttpSecurity http) 时的版本。
     * 对每个请求进行细粒度安全性控制的关键在于重载 configure(HttpSecurity) 方法。
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 通过调用 authorizeRequests() 和 anyRequest().authenticated() 就会要求所有进入应用的 HTTP 请求都要进行认证。它也配置 Spring Security 支持
        // 基于表单的登录以及 HTTP Basic 方式的认证。此时还需要重载 configure(AuthenticationManagerBuilder) 方法，不然没有用户存储支撑认证过程。没有用户
        // 存储，实际上就等于没有用户。所以，在这里所有的请求都需要认证，但是没有人能够登录成功。
        http.authorizeRequests().anyRequest().authenticated()
                .and()
                // formLogin() 方法启用了基本的登录页功能，也就是启用了默认的登录页
                .formLogin()
                .and()
                .httpBasic();

        // 为不同的 URL 路径有选择地应用安全性。
        // 除了路径选择，我们还通过 authenticated() 和 permitAll() 来定义该如何保护路径。authenticated() 要求在执行该请求时，必须已经登录了应用。
        // 如果用户没有认证的话，Spring Security 的 Filter 将会捕获该请求，并将用户重定向到应用的登录页面。同时，permitAll() 方法允许请求没有任何的安全限制。
        http.authorizeRequests()
                // 第一次调用 antMatchers() 指定了对 “/spitters/me” 路径的请求需要进行认证。
                .antMatchers("/spitters/me").authenticated()
                // 第二次调用 antMatchers() 更为具体，说明对 “/spittles” 路径的 HTTP POST 请求必须要经过认证。
                .antMatchers(HttpMethod.POST, "/spittles").authenticated()
                // antMatchers() 方法中设定的路径支持 Ant 风格的通配符。
                .antMatchers("/spitters/**").authenticated()
                // 也可以在一个对antMatchers()方法的调用中指定多个路径
                .antMatchers("/spitters/**", "/spittles/mime").authenticated()
                // antMatchers() 方法所使用的路径可能会包括Ant风格的通配符，而 regexMatchers() 方法则能够接受正则表达式来定义请求路径。
                // 如下代码片段所使用的正则表达式与 “/spitters/**”（Ant风格）功能是相同的。
                .regexMatchers("/spitters/.*").authenticated()
                // 最后对 anyRequests() 的调用中，说明其他所有的请求都是允许的，不需要认证和任何的权限。
                .anyRequest().permitAll();

        /**
         * 除了 authenticated() 和 permitAll() 以外，还有其他的一些方法能够用来定义该如何保护请求。
         * 1、access(String)：如果给定的 SpEL 表达式计算结果为 true，就允许访问。
         * 2、anonymous()：允许匿名用户访问。
         * 3、authenticated()：允许认证过的用户访问。
         * 4、denyAll()：无条件拒绝所有访问。
         * 5、fullyAuthenticated()：如果用户是完整认证的话（不是通过 Remember-me 功能认证的），就允许访问。
         * 6、hasAnyAuthority(String...)：如果用户具备给定权限中的某一个的话，就允许访问。
         * 7、hasAnyRole(String...)：如果用户具备给定角色中的某一个的话，就允许访问。
         * 8、hasAuthority(String)：如果用户具备给定权限的话，就允许访问。
         * 9、hasIpAddress(String)：如果请求来自给定 IP 地址的话，就允许访问。
         * 10、hasRole(String)：如果用户具备给定角色的话，就允许访问。
         * 11、not()：对其他访问方法的结果求反。
         * 12、permitAll()：无条件允许访问。
         * 13、rememberMe()：如果用户是通过 Remember-me 功能认证的，就允许访问。
         * 只是上面的大多数方法都是一维的，也就是说我们可以使用 hasRole() 限制某个特定的角色，但是我们不能在相同的路径上同时通过 hasIpAddress() 限制特定的 IP 地址。
         */

        // 可以修改之前的 configure() 方法，要求用户不仅需要认证，还要具备 ROLE_SPITTER 权限：
        http.authorizeRequests()
                .antMatchers("/spitters/me").hasAuthority("ROLE_SPITTER")
                .antMatchers(HttpMethod.POST, "spittles").hasAuthority("ROLE_SPITTER")
                .anyRequest().permitAll();
        // 作为替代方案，我们还可以使用 hasRole() 方法，它会自动使用 “ROLE_” 前缀：
        http.authorizeRequests()
                .antMatchers("/spitters/me").hasRole("SPITTER")
                .antMatchers(HttpMethod.POST, "spittles").hasRole("SPITTER")
                .anyRequest().permitAll();

        /**
         * 我们可以将任意数量的 antMatchers()、regexMatchers() 和 anyRequest() 连接起来，以满足 Web 应用安全规则的需要。但是，我们需要知道，
         * 这些规则会按照给定的顺序发挥作用。所以，很重要的一点就是将最为具体的请求路径放在前面，而最不具体的路径（如 anyRequest()）放在最后面。
         * 如果不这样做的话，那不具体的路径配置将会覆盖掉更为具体的路径配置。
         */

        // 借助 access() 方法，我们也可以将 SpEL 作为声明访问限制的一种方式。
        http.authorizeRequests()
                // 如下就是使用 SpEL 表达式来声明具有 “ROLE_SPITTER” 角色才能访问 “/spitter/me” URL：
                // 这个对 “/spitter/me” 的安全限制与开始时的效果是等价的，只不过这里使用了 SpEL 来描述安全规则。如果当前用户被授予了给定角色的
                // 话，那 hasRole() 表达式的计算结果就为 true。
                .antMatchers("/spitter/me").access("hasRole('ROLE_SPITTER')");

        /**
         * 让 SpEL 更强大的原因在于，hasRole() 仅是 Spring 支持的安全相关表达式中的一种，下面列出了 Spring Security 支持的所有 SpEL 表达式。
         * 1、authentication：用户的认证对象。
         * 2、denyAll：结果始终为 false。
         * 3、hasAnyRole(list of roles)：如果用户被授予了列表中任意的指定角色，结果为 true。
         * 4、hasRole(role)：如果用户被授予了指定的角色，结果为 true。
         * 5、hasIpAddress(IP Address)：如果请求来自指定 IP 的话，结果为 true。
         * 6、isAnonymous()：如果当前用户为匿名用户，结果为 true。
         * 7、isAuthenticated()：如果当前用户进行了认证的话，结果为 true。
         * 8、isFullyAuthenticated()：如果当前用户进行了完整认证的话（不是通过 Remember-me 功能进行的认证），结果为 true。
         * 9、isRememberMe()：如果当前用户是通过 Remember-me 自动认证的，结果为 true。
         * 10、permitAll：结果始终为 true。
         * 11、principal：用户的 principal 对象。
         */

        // 我们可以使用 SpEL 实现各种各样的安全性限制。例如，如果你想限制 “/spitter/me” URL 的访问，不仅需要 ROLE_SPITTER， 需要来自指定的 IP 地址，
        // 那么我们可以按照如下的方式调用 access() 方法：
        http.authorizeRequests()
                .antMatchers("/spitter/me").access("hasRole('ROLE_SPITTER') and hasIpAddress('192.168.1.2')");

        http.authorizeRequests()
                .antMatchers("/spitter/me").hasRole("SPITTER")
                .antMatchers(HttpMethod.POST, "/spittles").hasRole("SPITTER")
                .anyRequest().permitAll()
                .and()
                // 借助这个方法能够为各种 URL 模式声明所要求的通道。
                // requiresChannel() 方法会为选定的URL强制使用 HTTPS
                .requiresChannel()
                // 不论何时， 只要是对 “/spitter/form” 的请求，Spring Security 都视为需要安全通道（通过调用
                // requiresChannel() 确定的）并自动将请求重定向到 HTTPS 上。
                .antMatchers("/spitter/form").requiresSecure()
                // 与之相反，有些页面并不需要通过 HTTPS 传送。例如，首页不包含任何敏感信息，因此并不需要通过 HTTPS 传送。
                // 我们可以使用 requiresInsecure() 代替 requiresSecure() 方法，将首页声明为始终通过 HTTP 传送：
                // 如果通过 HTTPS 发送了对 “/” 的请求，Spring Security 将会把请求重定向到不安全的 HTTP 通道上。
                .antMatchers("/").requiresInsecure();

        // 处理 CSRF 的另外一种方式就是根本不去处理它。我们可以在配置中通过调用 csrf().disable() 禁用 Spring Security 的 CSRF 防护功能
        http.csrf().disable();

        /**
         * 对于应用程序的人类用户来说，基于表单的认证是比较理想的。当应用程序的使用者是另外一个应用程序的话（RESTful API），使用表单来提示登录的方式就不太适合了。
         * HTTP Basic 认证（HTTP Basic Authentication）会直接通过 HTTP 请求本身，对要访问应用程序的用户进行认证。你可能在以前见过 HTTP Basic 认证。
         * 当在 Web 浏览器中使用时，它将向用户弹出一个简单的模态对话框。但这只是 Web 浏览器的显示方式。本质上，这是一个 HTTP 401 响应，表明必须要在请求中包含
         * 一个用户名和密码。在 REST 客户端向它使用的服务进行认证的场景中，这种方式比较适合。
         */

        /**
         * 如果要启用 HTTP Basic 认证的话，只需在 configure() 方法所传入的 HttpSecurity 对象上调用 httpBasic() 即可。另外，还可以通过调用 realmName()
         * 方法指定域。如下是在 Spring Security 中启用 HTTP Basic 认证的典型配置：
         */
        http.formLogin()
                .loginPage("/login")
                .and()
                // HTTP Basic 认证要么开启要么关闭
                .httpBasic()
                .realmName("Spittr");

        /**
         * Spring Security 使得为应用添加 Remember-me 功能变得非常容易。为了启用这项功能，只需在 configure() 方法所传入的 HttpSecurity 对象上调用
         * rememberMe() 即可。默认情况下，这个功能是通过在 cookie 中存储一个 token 完成的，这个 token 最多两周内有效。但是，在这里，我们指定这个 token 最多四周
         * 内有效（2,419,200秒）。存储在 cookie 中的 token 包含用户名、密码、过期时间和一个私钥——在写入 cookie 前都进行了 MD5 哈希。默认情况下，私钥的名
         * 为 SpringSecured，但在这里我们将其设置为 spitterKey，使它专门用于 Spittr 应用。
         */
        http.formLogin().loginPage("/login")
                .and()
                .rememberMe().tokenValiditySeconds(2419200).key("spittrKey");

        /**
         * 退出功能是通过 Servlet 容器中的 Filter 实现的（默认情况下），这个 Filter 会拦截针对 “/logout” 的请求。因此，为应用添加退出功能只需添加如下的链接即可
         * （如下以 Thymeleaf 代码片段的形式进行了展现）：<a th:href="@{/logout}">Logout</a>。当用户点击这个链接的时候，会发起对 “/logout” 的请求，这个请求会
         * 被 Spring Security 的 LogoutFilter 所处理。用户会退出应用，所有的 Remember-me token 都会被清除掉。在退出完成后，用户浏览器将会重定向到 “/login?logout”，
         * 从而允许用户进行再次登录。如果你希望用户被重定向到其他的页面，如应用的首页，那么可以在 configure() 中进行如下的配置：
         */
        http.formLogin().loginPage("/login")
                .and()
                .logout()
                // 调用 logoutSuccessUrl() 表明在退出成功之后，浏览器需要重定向到 “/”
                .logoutSuccessUrl("/")
                // 重写默认的 LogoutFileter 拦截路径，也就是触发退出时进行处理的 URL
                .logoutUrl("/signout");
    }

    /**
     * 因为我们的安全配置类扩展了 WebSecurityConfigurerAdapter，因此配置用户存储的最简单方式就是重载 configure() 方法，并以 AuthenticationManagerBuilder
     * 作为传入参数。AuthenticationManagerBuilder 有多个方法可以用来配置 Spring Security 对认证的支持。通过 inMemoryAuthentication() 方法，我们可以启用、
     * 配置并任意填充基于内存的用户存储。
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 启用内存用户存储并使用两个用户来配置内存用户存储。
        // configure() 方法中的 AuthenticationManagerBuilder 使用构造者风格的接口来构建认证配置。
        auth.inMemoryAuthentication()
                .withUser("user").password("password").roles("USER").and()
                .withUser("admin").password("password").roles("USER","ADMIN");

        // 需要注意的是，roles() 方法是 authorities() 方法的简写形式。roles() 方法所给定的值都会添加一个 “ROLE_” 前缀，并将其作为权限授予给用户。
        // 下面的用户配置与上面的用户配置是等价的
        auth.inMemoryAuthentication()
                .withUser("user").password("password").authorities("ROLE_USER").and()
                .withUser("admin").password("password").authorities("ROLE_USER","ROLE_ADMIN");

        /**
         * withUser() 方法返回的是 UserDetailsManagerConfigurer.UserDetailsBuilder，下面描述了该对象所有可用的方法：
         * 1、accountExpired(boolean)：定义账号是否已经过期。
         * 2、accountLocked(boolean)：定义账号是否已经锁定。
         * 3、and()：用来连接配置。
         * 4、authorities(GrantedAuthority...)：授予某个用户一项或多项权限。
         * 5、authorities(List<? extends GrantedAuthority>)：授予某个用户一项或多项权限。
         * 6、authorities(String...)：授予某个用户一项或多项权限。
         * 7、credentialsExpired(boolean)：定义凭证是否已经过期。
         * 8、disabled(boolean)：定义账号是否已被禁用。
         * 9、password(String)：定义用户的密码。
         * 10、roles(String...)：授予某个用户一项或多项角色。
         */

        // ================================================================================================================================

        // 用户数据通常会存储在关系型数据库中，并通过 JDBC 进行访问。为了配置 Spring Security 使用以 JDBC 为支撑的用户存储，我们可以使用 jdbcAuthentication() 方法。
        // 下面是所需的最少配置。
        auth.jdbcAuthentication().dataSource(dataSource);

        // 尽管默认的最少配置能够让一切运转起来，但是它对我们的数据库模式有一些要求。它预期存在某些存储用户数据的表。更具体来说，下面的代码片段来源于 Spring Security 内部，
        // 这块代码展现了当查找用户信息时所执行的 SQL 查询语句：
        final String DEF_USERS_BY_USERNAME_QUERY =
                "select username,password,enabled " +
                "from users " +
                "where username = ?";
        final String DEF_AUTHORITIES_BY_USERNAME_QUERY =
                "select username,authority " +
                "from authorities " +
                "where username = ?";
        final String DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY =
                "select g.id, g.group_name, ga.authority " +
                "from groups g, group_members gm, group_authorities ga " +
                "where gm.username = ? " +
                "and g.id = ga.group_id " +
                "and g.id = gm.group_id";
        // 在第一个查询中，我们获取了用户的用户名、密码以及是否启用的信息，这些信息会用来进行用户认证。接下来的查询查找了用户所授予的权限，用来进行鉴权，最后一个查询中，
        // 查找了用户作为群组的成员所授予的权限。如果你能够在数据库中定义和填充满足这些查询的表，那么基本上就不需要你再做什么额外的事情了。但是，也有可能你的数据库与上面
        // 所述并不一致，那么你就会希望在查询上有更多的控制权。如果是这样的话，我们可以按照如下的方式配置自己的查询：
        auth.jdbcAuthentication().dataSource(dataSource)
                .usersByUsernameQuery("select username,password,true from Spitter where username = ?")
                .authoritiesByUsernameQuery("select username,'ROLE_USER' from Spitter where username = ?")
                // 借助 passwordEncoder() 方法指定一个密码转码器（encoder）：passwordEncoder() 方法可以接受 Spring Security 中 PasswordEncoder 接口的任意实现。
                // Spring Security 的加密模块包括了三个这样的实现：BCryptPasswordEncoder、NoOpPasswordEncoder 和 StandardPasswordEncoder。但是如果内置的实现
                // 无法满足需求时，可以提供自定义的实现。不管你使用哪一个密码转码器，都需要理解的一点是，数据库中的密码是永远不会解码的。所采取的策略与之相反，用户在登录时输入的
                // 密码会按照相同的算法进行转码，然后再与数据库中已经转码过的密码进行对比。这个对比是在 PasswordEncoder 的 matches() 方法中进行的。
                .passwordEncoder(new StandardPasswordEncoder("53cr3t"));
        // 将默认的 SQL 查询替换为自定义的设计时，很重要的一点就是要遵循查询的基本协议。所有查询都将用户名作为唯一的参数。认证查询会选取用户名、密码以及启用状态信息。
        // 权限查询会选取零行或多行包含该用户名及其权限信息的数据。群组权限查询会选取零行或多行数据，每行数据中都会包含群组ID、群组名称以及权限。因为从数据库的查询结果
        // 要转成符合 Spring Security 的用户数据对象，所以查询结果是固定的那些字段。

        // =================================================================================================================================

        // 为了让 Spring Security 使用基于 LDAP 的认证，我们可以使用 ldapAuthentication() 方法。这个方法在功能上类似于 jdbcAuthentication()，只不过是 LDAP 版本。
        // 方法 userSearchFilter() 和 groupSearchFilter() 用来为基础 LDAP 查询提供过滤条件，它们分别用于搜索用户和组。
        auth.ldapAuthentication()
                .userSearchFilter("(uid={0})")
                .groupSearchFilter("member={0}");

        // 默认情况下，对于用户和组的基础查询都是空的，也就是表明搜索会在 LDAP 层级结构的根开始。但是我们可以通过指定查询基础来改变这个默认行为：
        auth.ldapAuthentication()
                // userSearchBase() 属性为查找用户提供了基础查询，声明用户应该在名为 people 的组织单元下搜索而不是从根开始。
                .userSearchBase("ou=people")
                .userSearchFilter("(uid={0})")
                // groupSearchBase()为查找组指定了基础查询，组应该在名为groups的组织单元下搜索。
                .groupSearchBase("ou=groups")
                .groupSearchFilter("member={0}");

        // 基于 LDAP 进行认证的默认策略是进行绑定操作，直接通过 LDAP 服务器认证用户。另一种可选的方式是进行比对操作。这涉及将输入的密码发送到 LDAP 目录上，
        // 并要求服务器将这个密码和用户的密码进行比对。因为比对是在 LDAP 服务器内完成的，实际的密码能保持私密。如果希望通过密码比对进行认证，可以通过声明
        // passwordCompare() 方法来实现：
        auth.ldapAuthentication()
                .userSearchBase("ou=people")
                .userSearchFilter("(uid={0})")
                .groupSearchBase("ou=groups")
                .groupSearchFilter("member={0}")
                .passwordCompare()
                // 我们还可以指定密码转码器。在进行服务器端密码比对时，有一点非常好，那就是实际的密码在服务器端是私密的。但是进行尝试的密码还是需要通过线路
                // 传输到LDAP服务器上，这可能会被黑客所拦截。为了避免这一点，我们可以通过调用 passwordEncoder() 方法指定加密策略。在本示例中，密码会
                // 进行 MD5 加密。这需要 LDAP 服务器上密码也使用 MD5 进行加密。
                // Md5PasswordEncoder 在 Spring Security 5 中已经被删除了，在 Spring Security 3 中还在用
                .passwordEncoder(new Md5PasswordEncoder())
                // 默认情况下，在登录表单中提供的密码将会与用户的 LDAP 条目中的 userPassword 属性进行比对。如果密码被保存在不同的属性中，可以通过
                // passwordAttribute() 方法来声明密码属性的名称。我们指定了要与给定密码进行比对的是 “passcode” 属性。
                .passwordAttribute("password");

        // 默认情况下，Spring Security 的 LDAP 认证假设 LDAP 服务器监听本机的 33389 端口。但是，如果你的 LDAP 服务器在另一台机器上，那么可以
        // 使用 contextSource() 方法来配置这个地址：
        auth.ldapAuthentication()
                .userSearchBase("ou=people")
                .userSearchFilter("(uid={0})")
                .groupSearchBase("ou=groups")
                .groupSearchFilter("member={0}")
                // contextSource() 方法会返回一个 ContextSourceBuilder 对象，这个对象除了其他功能以外，还提供了 url() 方法用来指定 LDAP 服务器的地址。
                .contextSource().url("ldap://habuma.com:389/dc=habuma,dc=com");

        // 如果你没有现成的 LDAP 服务器供认证使用，Spring Security 还为我们提供了嵌入式的 LDAP 服务器。我们不再需要设置远程 LDAP 服务器的 URL，
        // 只需通过 root() 方法指定嵌入式服务器的根前缀就可以了：
        auth.ldapAuthentication()
                .userSearchBase("ou=people")
                .userSearchFilter("(uid={0})")
                .groupSearchBase("ou=groups")
                .groupSearchFilter("member={0}")
                .contextSource().root("dc=habuma,dc=com")
                // 当 LDAP 服务器启动时，它会尝试在类路径下寻找 LDIF 文件来加载数据。LDIF（LDAP Data Interchange Format，LDAP 数据交换格式）是以
                // 文本文件展现 LDAP 数据的标准方式。每条记录可以有一行或多行，每项包含一个名值对。记录之间通过空行进行分割。如果你不想让 Spring 从整个
                // 根路径下搜索 LDIF 文件的话，那么可以通过调用 ldif() 方法来明确指定加载哪个 LDIF 文件：
                .ldif("classpath:users.ldif");

        // 为了使用自定义的 SpitterUserSerivce 来认证用户，我们可以通过 userDetailsService() 方法将其设置到安全配置中：
        // userDetailsService() 方法（类似于 jdbcAuthentication()、ldapAuthentication 以及 inMemoryAuthentication()）会配置一个用户存储。
        // 不过，这里所使用的不是 Spring 所提供的用户存储，而是使用 UserDetailsService 的实现。
        auth.userDetailsService(new SpitterUserService(spitterRepository));

        // 另外一种值得考虑的方案就是修改 Spitter，让其实现 UserDetails。这样的话，loadUserByUsername() 就能直接返回 Spitter 对象了，
        // 而不必再将它的值复制到 User 对象中。
    }
}
